Skip to content

fix(typescript): guard against null/undefined in file upload core utility#16708

Open
cadesark wants to merge 4 commits into
mainfrom
devin/1782396911-fix-ts-file-upload-null-crash
Open

fix(typescript): guard against null/undefined in file upload core utility#16708
cadesark wants to merge 4 commits into
mainfrom
devin/1782396911-fix-ts-file-upload-null-crash

Conversation

@cadesark

@cadesark cadesark commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Description

Fixes a regression (v3.12.x → v3.68.0) where passing null or undefined to an optional multipart file upload field crashes the SDK with TypeError: Cannot use 'in' operator to search for 'path' in undefined.

Changes Made

appendFile throws on null/undefined (in FormDataWrapper):

public async appendFile(key: string, value: Uploadable): Promise<void> {
    if (value == null) {
        throw new Error(
            `File upload for "${key}" received ${value === null ? "null" : "undefined"}. The generated code should not call appendFile with null/undefined — check that optional file fields are guarded before this call.`
        );
    }
    // ...
}

This is safe because the generated code already guards optional file fields:

if (request.maybe_file != null) {
    await _body.appendFile("maybe_file", request.maybe_file);
}

Defensive throw in getFileWithMetadata (internal function — if null somehow bypasses the guard):

if (file == null) {
    throw new Error(`Expected file to be a Blob, Buffer, ReadableStream, ...`);
}

toMultipartDataPart — clean signature (no | undefined return, no internal null guard). It assumes callers have already validated.

  • Uses Error (not TypeError) for consistency with line 86 in the same file
  • Unit tests added for null/undefined paths in toBinaryUploadRequest
  • All 35 seed file.ts, 12 seed FormDataWrapper.ts, and 33 seed test files updated

Testing

  • Unit tests added for null/undefined input
  • All seed test outputs updated and passing (50/50 CI checks green)
  • Updated README.md generator (if applicable) — N/A

Link to Devin session: https://app.devin.ai/sessions/4ae2868a605043aeab6adfcd30d22593
Requested by: @cadesark

…revent cryptic TypeError on file uploads

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@cadesark cadesark requested a review from Swimburger as a code owner June 25, 2026 14:16
@cadesark cadesark self-assigned this Jun 25, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@github-actions

github-actions Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

SDK Generation Benchmark Results

Comparing PR branch against median of 5 nightly run(s) on main (latest: 2026-06-26T05:24:50Z).

Full benchmark table (click to expand)
Generator Spec main (generator) main (E2E) PR (generator) Delta
ts-sdk square 236s (n=5) 238s (n=5) 221s -15s (-6.4%)

main (generator): generator-only time via --skip-scripts (includes Docker image build, container startup, IR parsing, and code generation — this is the same Docker-based flow customers use via fern generate). main (E2E): full customer-observable time including build/test scripts (nightly baseline, informational). Delta is computed against generator-only baseline.
⚠️ = generation exited with a non-zero exit code (timing may not reflect a successful run).
Baseline from nightly runs on main (latest: 2026-06-26T05:24:50Z). Trigger benchmark-baseline to refresh.
Last updated: 2026-06-26 20:43 UTC

…kip instead of crashing

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

@Swimburger Swimburger left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at this code, I think the check for null'ish should be higher up, and enforced so null'ish doesn't ever reach the code that you're changing here.

): Promise<{ data: Uploadable.FileLike; filename?: string; contentType?: string } | undefined> {
if (file == null) {
console.warn(`File upload received ${file === null ? "null" : "undefined"}, skipping.`);
return undefined;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since file is not null | undefined, I still think we should throw an error here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — since file is typed as Uploadable (not nullable), if it somehow reaches here as null it's a logic error and should throw rather than silently swallow. The tolerance/skip should happen at the appendFile level (the API boundary) before this function is ever called.

{ noSniffFileSize }: { noSniffFileSize?: boolean } = {},
): Promise<Uploadable.WithMetadata> {
if (file == null) {
throw new TypeError(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should use a different error type, not TypeError. Can you figure out what is most idiomatic in this case?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the rest of this file, line 90 uses throw new Error(...) for an invalid file upload. Using a plain Error would be most consistent with the existing code in this file. Will update.

Comment on lines +83 to +85
if (part == null) {
return;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't be necessary, as value should never be null'ish at this point, and part should never be null'ish at this point.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — moving the null guard to the top of appendFile (before calling toMultipartDataPart) makes this unnecessary. Will restructure so that appendFile guards early and these internal functions never see null.

…d in appendFile

- Changed throw new TypeError to throw new Error in getFileWithMetadata
  (consistent with line 86's Error usage)
- Moved null guard to appendFile level (before async toMultipartDataPart call)
- Reverted toMultipartDataPart to clean signature (no | undefined return type)
- Updated all seed output files to match

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@fern-support fern-support requested a review from Swimburger June 26, 2026 16:53
Comment on lines +82 to +85
if (value == null) {
console.warn(`File upload for "${key}" received ${value === null ? "null" : "undefined"}, skipping.`);
return;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should throw here. appendFile should not be allowed to be called with null.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The warn+skip behavior here was specifically requested to match the old 3.12.x SDK tolerance — the customer's optional file field passes undefined through the generated code into appendFile, and they expect it not to crash.

If the intent is to throw here instead, the fix would need to move upstream: the generated code that calls appendFile would need to guard optional file fields with if (value != null) before calling appendFile. Without that generated-code change, throwing here would reproduce the original crash (just with a clearer error message).

@cadesark — could you weigh in on the approach? Should we:

  1. Keep warn+skip in appendFile (current PR — tolerant core utility), or
  2. Throw in appendFile but also fix the generated caller code to not call appendFile with null for optional fields?

Option 2 is the cleaner design but requires changes to the TypeScript SDK generator's request body serialization.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — appendFile now throws an Error instead of warn+skip. The generated code already guards optional file fields with if (request.maybe_file != null) before calling appendFile, so this is safe.

}

public async appendFile(key: string, value: Uploadable): Promise<void> {
if (value == null) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should throw here. appendFile should not be allowed to be called with null.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above — this is part of the same decision. If we throw in appendFile, the null guard for optional file fields needs to live in the generated caller code instead.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — now throws.


public async appendFile(key: string, value: Uploadable): Promise<void> {
if (value == null) {
console.warn(`File upload for "${key}" received ${value === null ? "null" : "undefined"}, skipping.`);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should throw here. appendFile should not be allowed to be called with null.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above — this is part of the same decision. If we throw in appendFile, the null guard for optional file fields needs to live in the generated caller code instead.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — now throws.

appendFile should never be called with null/undefined — the generated
code already guards optional file fields with null checks before calling
appendFile. If null somehow reaches appendFile, it's a bug in the caller
that should surface immediately as an error rather than silently skipping.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants